/**
 * Copyright (c) 2012, David Varbel
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * 
 * 1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * 2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.epistrata.neutraltheoryapplet;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.Ellipse2D;
import java.util.HashMap;
import java.util.List;

import javax.swing.JOptionPane;
import javax.swing.JPanel;

import com.epistrata.experiment.Model;
import com.epistrata.experiment.Run;
import com.epistrata.experiment.View;
import com.epistrata.neutraltheory.Critter;
import com.epistrata.neutraltheory.NeutralTheoryModel;

public class PopulationPanelView extends JPanel implements View {
	private static final long serialVersionUID = 8980841385547692006L;
	private final int width = 800;
	private final int height = 600;
	boolean pause = false;
	boolean pcaView = false;
	private Rainbow rainbow;
	PrincipalComponentAnalysis pca;
	private double pcaMaxInEigenSpace;

	
	private class Rainbow {
		private float h;
		private float s;
		private float v;
		private float hMax = 360;
		private float hMin = 0;
		private float sMax = 1.0f;
		private float sMin = 0.5f;
		private float vMax = 1.0f;
		private float vMin = 0.6f;
		private float hInc = 83;
		private float sInc = 0.17f;
		private float vInc = 0.07f;
		
		public Rainbow() {
			h = 125f;  //hue
			s = 1.0f;  	//saturation
			v = 0.66f; 	//value
		}
		
		public Color nextColor() {
			//get current color in RGB
			float[] rgb = currentHsvToRgb();
			
			//move to next color
			h += hInc;
			if (h > hMax) {
				h = hMin + (h - hMax);
			}
			s += sInc;
			if (s > sMax) {
				s = sMin + (s - sMax);
			}
			v += vInc;
			if (v > vMax) {
				v = vMin + (v - vMax);
			}
			
			return new Color(rgb[0], rgb[1], rgb[2]);
		}
		
		/*
		 * Algorithm from Wikipedia: http://en.wikipedia.org/wiki/HSL_and_HSV#Converting_to_RGB.
		 * Accessed:  3/13/2012
		 */
		private float[] currentHsvToRgb() {
			float c = v * s;
			float hPrime = h / 60f;
			float x = c * (1 - Math.abs((hPrime % 2) - 1));
			float r1, g1, b1;
			if (hPrime < 1f) {
				r1 = c;
				g1 = x;
				b1 = 0;
			} else if (1f <= hPrime && hPrime < 2f) {
				r1 = x;
				g1 = c;
				b1 = 0;
			} else if (2f <= hPrime && hPrime  < 3f) {
				r1 = 0;
				g1 = c;
				b1 = x;
			} else if (3f <= hPrime && hPrime  < 4f) {
				r1 = 0;
				g1 = x;
				b1 = c;
			} else if (4f <= hPrime && hPrime  < 5f) {
				r1 = x;
				g1 = 0;
				b1 = c;
			} else if (5f <= hPrime && hPrime  <= 6f) {
				r1 = c;
				g1 = 0;
				b1 = x;
			} else {
				throw new IllegalStateException("Invalid H prime value in HSV to RGB conversion for rainbow color.");
			}
			float m = v - c;
			float[] rgb = new float[3];
			rgb[0] = r1 + m;
			rgb[1] = g1 + m;
			rgb[2] = b1 + m;
			
			return rgb;
		}
	}
	
	PopulationPanelView() {
		setBackground(Color.white);
	    setPreferredSize(new Dimension(width, height));
	}

	@Override
	public void handleRunStart(Run run) throws Exception {

	}

	@Override
	public void handleRunFinish(Run run) throws Exception {
		// TODO Auto-generated method stub

	}

	@Override
	public void handleDataUpdate(Run run, Model model) throws Exception {
		//render to image
		Image image = render((NeutralTheoryModel) model);
		
		//send image to screen
		refreshImage(image);
		
	    while (pause) {
	    	Thread.sleep(100);
	    	
	    	//so you can toggle view back and forth while paused
	    	if (pcaView) {
	    		image = render((NeutralTheoryModel) model);
	    		refreshImage(image);
	    	} else {
	    		image = render((NeutralTheoryModel) model);
	    		refreshImage(image);
	    	}
	    }
	    pca = null;
	}
	
	public void refreshImage(Image image) {
	    try {
			Graphics g = this.getGraphics();
			if ((g != null) && (image != null)) {
				g.drawImage(image, 0, 0, null);
				g.dispose();
			}
	    } catch (Exception e) {
	    	JOptionPane.showMessageDialog(this, "Graphics context error: " + e);
	    }
		
	}

	private Image render(NeutralTheoryModel model) {
		Image image = createImage(width, height);
		Graphics2D g = (Graphics2D) image.getGraphics();
	    rainbow = new Rainbow();
		
		List<Critter> population = model.getPopulation();

		double worldX = model.getWorldXSize();
		double worldY = model.getWorldYSize();
		
		double critterSize = 10;
		
		HashMap<Integer, Color> speciesColorMap = new HashMap<Integer, Color>();
		
		for (Critter critter : population) {
				
			//map location onto panel coordinates
			double[] loc = critter.getLocation();
			loc[0] = (loc[0] * width) / worldX;
			loc[1] = (loc[1] * height) / worldY;
			
			if (pcaView) {
	    		if (pca == null) {
	    			calcPca(((NeutralTheoryModel) model).getPopulation());
	    		}

				double[] genome = critter.getDoubleGenomeForPca();
				//map high dimensional genome onto 3 dimensional space
				double[] threeDimMap = pca.sampleToEigenSpace(genome);
				
				//scale values from 3d space onto RGB, and swap order so blue is the represented by the most significant dimension
				float blue = (float) ((threeDimMap[0] + pcaMaxInEigenSpace) / (pcaMaxInEigenSpace * 2));
				float red = (float) ((threeDimMap[1] + pcaMaxInEigenSpace) / (pcaMaxInEigenSpace * 2));
				float green = (float) ((threeDimMap[2] + pcaMaxInEigenSpace) / (pcaMaxInEigenSpace * 2));
				g.setPaint(new Color(red, green, blue));
			} else {
				int speciesIndex = critter.getSpeciesIndex();
				Color speciesColor = speciesColorMap.get(speciesIndex);
				if (speciesColor == null) {
					speciesColor = rainbow.nextColor();
					speciesColorMap.put(speciesIndex, speciesColor);
				}
				g.setPaint(speciesColor);
			}
			
			//draw circle for critter
			g.fill( new Ellipse2D.Double( 	loc[0] - (critterSize/2d),
											loc[1] - (critterSize/2d),
											critterSize,
											critterSize) );
		}
		
		/*
		 * test rainbow
		double startLoc = 100d;
		double finishLoc = 300d;
		for (double i = 0; i < 15; i++) {
			g.setPaint(rainbow.nextColor());
			double x = startLoc + ((finishLoc - startLoc) / 15) * i;
			double y = startLoc + ((finishLoc - startLoc) / 15) * i;
			g.fill( new Ellipse2D.Double( x -(critterSize/2d), y - (critterSize/2d), critterSize, critterSize) );
		}
		*/

		return image;
	}
	
	public void calcPca(List<Critter> population) {
		int genomeLength = population.get(0).getDoubleGenomeForPca().length;
		double[][] critterData = new double[population.size()][genomeLength];
		for (int i = 0; i < critterData.length; i++) {
			System.arraycopy(population.get(i).getDoubleGenomeForPca(), 0, critterData[i], 0, genomeLength);
		}
		pca = new PrincipalComponentAnalysis(critterData, 3);
		pca.computeBasis();
		
		//for the purposes of later scaling to RGB space, determine the max possible value in eigenspace
		double[][] calcMaxData = new double[4][genomeLength];
		//have data is all 1's, half data is all 0's
		//this creates the maximum possible distance between the points
		for (int i = 0; i < 2; i++) {
			for (int j = 0; j < genomeLength; j++) {
				calcMaxData[i][j] = 1.0; 
			}
		}
		for (int i = 2; i < 4; i++) {
			for (int j = 0; j < genomeLength; j++) {
				calcMaxData[i][j] = 0.0; 
			}
		}
		PrincipalComponentAnalysis maxAnalysis = new PrincipalComponentAnalysis(calcMaxData, 3);
		maxAnalysis.computeBasis();
		double[] convertedData = new double[genomeLength];
		convertedData = maxAnalysis.sampleToEigenSpace(calcMaxData[0]);
		/* 
		 * the first dimension axis in the new eigenspace is aligned with the "best fit" vector for the original data
		 * in this case, the best fit line is orthogonal to the vectors from the original data
		 * the magnitude of the first dimension of any of of the converted data points will thus be the maximum possible
		 * magnitude in any dimension of any possible data point, given the dimentionality of the original sample space
		 * and the fact that no magnitudes exceed 1.0 in the original sample space. 
		 */
		this.pcaMaxInEigenSpace = convertedData[0];
	}
	
	@Override
	public void handleMessage(Run run, String msg) throws Exception {
		// TODO Auto-generated method stub

	}

	@Override
	public void start() throws Exception {
		// TODO Auto-generated method stub

	}

	@Override
	public void close() throws Exception {
		// TODO Auto-generated method stub

	}
	
	public void setPause(boolean pause) {
		this.pause = pause; 
	}

	public void setPcaView(boolean pcaView) {
		this.pcaView = pcaView;
	}

}
